// Copyright (c) 2012-2013 Turbulenz Limited

/*global
Draw2D: false
Float32Array: false
*/

//
// Draw2DGroup. Wraps vertex buffer data with pairings of indices and textures
// representing subsets of buffer relating to a set of equal-texture quads.
//
// [ sprite1  sprite2  sprite3  sprite4  sprite5 ]
//  \---------------/  \------/ \--------------/
//       texture 1    texture 2     texture 3
//      12 indices    6 indices     12 indices
//
class Draw2DGroup
{
    indices          : number[];
    textures         : Texture[];
    numSets          : number;
    vertexBufferData : any; // new Draw2D.floatArray(1024);
    numVertices      : number;

    static create() : Draw2DGroup
    {
        var group = new Draw2DGroup();

        // pairs of index counts + associated texture for subset of group.
        group.indices = [];
        group.textures = [];
        group.numSets = 0;

        // vertex buffer for group.
        group.vertexBufferData = new Draw2D.floatArray(1024);
        group.numVertices = 0;

        return group;
    }
};

interface Draw2DSpriteParams
{
    x?: number;
    y?: number;
    width?: number;
    height?: number;
    texture?: Texture;
    color?: any; // v4
    rotation?: number;
    textureRectangle?: any; // v4 (left, top, right, bottom)
    scale?: number;
    shear?: number;
    origin?: any; // v2
}

//
// Draw2DSprite
//
class Draw2DSprite
{
    static version = 1;

    x                : number;
    y                : number;
    rotation         : number;

    private data     : any; // new Draw2D.floatArray(38);
    private _texture : Texture;

    //
    // Assumption is that user will not be performing these actions
    // frequently.  To that end, we provide a function which performs
    // the necessary side effects on call, to prevent an overhead for
    // lazy evaluation.
    //
    getTextureRectangle(dst)
    {
        if (dst === undefined)
        {
            dst = new Draw2D.floatArray(4);
        }
        var data = this.data;
        var texture = this._texture;
        if (texture)
        {
            dst[0] = data[12] * texture.width;
            dst[1] = data[13] * texture.height;
            dst[2] = data[14] * texture.width;
            dst[3] = data[15] * texture.height;
        }
        else
        {
            dst[0] = data[12];
            dst[1] = data[13];
            dst[2] = data[14];
            dst[3] = data[15];
        }
        return dst;
    }

    setTextureRectangle(uvRect)
    {
        var data = this.data;
        var texture = this._texture;
        if (texture)
        {
            var iwidth  = 1 / texture.width;
            var iheight = 1 / texture.height;
            data[12] = uvRect[0] * iwidth;
            data[13] = uvRect[1] * iheight;
            data[14] = uvRect[2] * iwidth;
            data[15] = uvRect[3] * iheight;
        }
        else
        {
            data[12] = uvRect[0];
            data[13] = uvRect[1];
            data[14] = uvRect[2];
            data[15] = uvRect[3];
        }
    }

    getColor(dst?)
    {
        if (dst === undefined)
        {
            dst = new Draw2D.floatArray(4);
        }
        var data = this.data;
        dst[0] = data[8];
        dst[1] = data[9];
        dst[2] = data[10];
        dst[3] = data[11];
        return dst;
    }

    setColor(color)
    {
        var data = this.data;
        data[8]  = color[0];
        data[9]  = color[1];
        data[10] = color[2];
        data[11] = color[3];
    }

    setColorRGB(r: number, g: number, b: number)
    {
        var data = this.data;
        data[8]  = r;
        data[9]  = g;
        data[10] = b;
    }

    getAlpha() : number
    {
        return this.data[11];
    }

    setAlpha(alpha: number)
    {
        this.data[11] = alpha;
    }

    getTexture() : Texture
    {
        return this._texture;
    }

    setTexture(texture)
    {
        if (this._texture !== texture)
        {
            var su = (this._texture ? this._texture.width  : 1.0) / (texture ? texture.width  : 1.0);
            var sv = (this._texture ? this._texture.height : 1.0) / (texture ? texture.height : 1.0);
            this._texture = texture || null;

            // re-normalise texture coordinates.
            var data = this.data;
            data[12] *= su;
            data[13] *= sv;
            data[14] *= su;
            data[15] *= sv;
        }
    }

    getWidth(): number
    {
        return this.data[17] * 2;
    }

    setWidth(width)
    {
        width *= 0.5;
        var data = this.data;
        if (data[17] !== width)
        {
            // Move the origin so that the sprite gets scaled around
            // it, rather than scaled around the top left corner.

            // originX = originX * (newwidth/2) / (oldwidth/2)
            data[23] = data[23] * width / data[17];
            data[17] = width;
            this._invalidate();
        }
    }

    getHeight(): number
    {
        return this.data[18] * 2;
    }

    setHeight(height)
    {
        height *= 0.5;
        var data = this.data;
        if (data[18] !== height)
        {
            // originY = originY * (newheight/2) / (oldheight/2)
            data[24] = data[24] * height / data[18];
            data[18] = height;
            this._invalidate();
        }
    }

    getScale(dst)
    {
        if (dst === undefined)
        {
            dst = new Draw2D.floatArray(2);
        }
        var data = this.data;
        dst[0] = data[19];
        dst[1] = data[20];
        return dst;
    }

    setScale(scale)
    {
        var scaleX = scale[0];
        var scaleY = scale[1];
        var data = this.data;
        if (data[19] !== scaleX || data[20] !== scaleY)
        {
            data[19] = scaleX;
            data[20] = scaleY;
            this._invalidate();
        }
    }

    getShear(dst)
    {
        if (dst === undefined)
        {
            dst = new Draw2D.floatArray(2);
        }
        var data = this.data;
        dst[0] = data[21];
        dst[1] = data[22];
        return dst;
    }

    setShear(shear)
    {
        var shearX = shear[0];
        var shearY = shear[1];
        var data = this.data;
        if (data[21] !== shearX || data[22] !== shearY)
        {
            data[21] = shearX;
            data[22] = shearY;
            this._invalidate();
        }
    }

    getOrigin(dst)
    {
        if (dst === undefined)
        {
            dst = new Draw2D.floatArray(2);
        }
        var data = this.data;
        dst[0] = data[23];
        dst[1] = data[24];
        return dst;
    }

    setOrigin(origin)
    {
        var originX = origin[0];
        var originY = origin[1];
        var data = this.data;
        if (data[23] !== originX || data[24] !== originY)
        {
            data[23] = originX;
            data[24] = originY;
            this._invalidate();
        }
    }

    // Method for internal use only.
    //
    // Recompute locally defined vectors.
    private _invalidate()
    {
        var data = this.data;
        // [ T1 T2 ] = [ scaleX 0 ] [ 1 shearX ]
        // [ T3 T4 ]   [ 0 scaleY ] [ shearY 1 ]
        var T1 = data[19];
        var T2 = data[19] * data[21];
        var T3 = data[20] * data[22];
        var T4 = data[20];

        // Recompute locally defined position of true center of sprite.
        var x = data[17] - data[23];  // x = width/2 - originX
        var y = data[18] - data[24];  // y = height/2 - originY
        var cx = data[25] = (T1 * x + T2 * y); // (cx) = T (x)
        var cy = data[26] = (T3 * x + T4 * y); // (cy)     (y)

        // Recompute locally defined position of top-left vertex relative to center of sprite.
        x = -data[17]; // x = -width/2
        y = -data[18]; // y = -height/2
        var ux = data[27] = (T1 * x + T2 * y); // (ux) = T (x)
        var uy = data[28] = (T3 * x + T4 * y); // (uy)     (y)

        // Recompute locally defined position of top-right vertex relative to center of sprite.
        x = -x; // x = width / 2
        var vx = data[29] = (T1 * x + T2 * y); // (vx) = T (x)
        var vy = data[30] = (T3 * x + T4 * y); // (vy)     (y)

        // Rotate vectors to screen space so that in the case that rotation is not performed
        // These vectors are still valid.
        var rotation = data[16] = this.rotation;
        var cos = Math.cos(rotation);
        var sin = Math.sin(rotation);

        data[31] = ((cos * cx) - (sin * cy));
        data[32] = ((sin * cx) + (cos * cy));
        data[33] = ((cos * ux) - (sin * uy));
        data[34] = ((sin * ux) + (cos * uy));
        data[35] = ((cos * vx) - (sin * vy));
        data[36] = ((sin * vx) + (cos * vy));

        // Compute suitable epsilon to consider rotations equals.
        // We do this by finding the vertex furthest from defined center of rotation.
        // And using its distance to compute what rotation constitutes a 'visible' rotation.
        //
        // Positions of vertices relative to origin are given by:
        // v1 = c + u, v2 = c + v, v3 = c - v, v4 = c - u.
        // |v1|^2 = |c|^2 + |u|^2 + 2c.u
        // |v4|^2 = |c|^2 + |u|^2 - 2c.u
        // |v2|^2 = |c|^2 + |v|^2 + 2c.v
        // |v3|^2 = |c|^2 + |v|^2 - 2c.v
        //
        // Compute r1 = |u|^2 + abs(2c.u)
        // Compute r2 = |v|^2 + abs(2c.v)
        //
        // Finally max(|vi|^2) = |c|^2 + max(r1, r2)
        //
        var dot = 2 * ((cx * ux) + (cy * uy));
        if (dot < 0)
        {
            dot = -dot;
        }
        var r1 = (ux * ux) + (uy * uy) + dot;

        dot = 2 * ((cx * vx) + (cy * vy));
        if (dot < 0)
        {
            dot = -dot;
        }
        var r2 = (vx * vx) + (vy * vy) + dot;

        if (r2 > r1)
        {
            r1 = r2;
        }

        r1 += ((cx * cx) + (cy * cy));
        // r1 is the squared distance to furthest vertex.
        //
        // We permit a half pixel movement to be considered a 'true' movement.
        // Squared rotation required to impart this movement on furthest vertex is
        data[37] = (0.25 / r1); // squared epsilon
    }

    // Method for internal use only.
    //
    // Recompute draw2d coordinate space vertices and vectors.
    private _update(angleScaleFactor)
    {
        var data = this.data;
        var x, y, u, v;

        // Check if rotation has been modified
        x = this.rotation;
        y = x - data[16]; // y = rotation - previousRotation
        if ((y * y) > (data[37] * angleScaleFactor)) // if |y| > epsilon
        {
            data[16] = x; //previousRotation = rotation
            u = Math.cos(x);
            v = Math.sin(x);

            // rotate locally defined vectors.
            x = data[25];
            y = data[26];
            data[31] = (u * x - v * y); // (px) = [cos -sin] (cx)
            data[32] = (v * x + u * y); // (py) = [sin  cos] (cy)

            x = data[27];
            y = data[28];
            data[33] = (u * x - v * y); // (x1) = [cos -sin] (ux)
            data[34] = (v * x + u * y); // (y1) = [sin  cos] (uy)

            x = data[29];
            y = data[30];
            data[35] = (u * x - v * y); // (x2) = [cos -sin] (vx)
            data[36] = (v * x + u * y); // (y2) = [sin  cos] (vy)
        }

        // Compute center of this sprite in screen space.
        u = this.x + data[31]; // u = centerX = positionX + px
        v = this.y + data[32]; // v = centerY = positionY + py

        // Compute vertex positions in screen space.
        x = data[33];
        y = data[34];
        data[0] = u + x; // v1x = centerX + x1
        data[1] = v + y; // v1y = centerY + y1
        data[6] = u - x; // v4x = centerX - x1
        data[7] = v - y; // v4y = centerY - y1

        x = data[35];
        y = data[36];
        data[2] = u + x; // v2x = centerX + x2
        data[3] = v + y; // v2y = centerY + y2
        data[4] = u - x; // v3x = centerX - x2
        data[5] = v - y; // v3y = centerY - y2
    }

    static create(params: Draw2DSpriteParams): Draw2DSprite
    {
        if ((params.width === undefined || params.height === undefined) && !params.texture)
        {
            return null;
        }

        // data:
        // ---
        // First 16 values reserved for Draw2DSpriteData.
        //   includes colour and texture coordinates.
        //
        // 16    : old_rotation (for lazy evaluation)
        // 17,18 : width/2, height/2 (changed by user via function)
        // 19,20 : scaleX, scaleY    (changed by user via function)
        // 21,22 : shearX, shearY    (changed by user via function)
        // 23,24 : originX, originY  (changed by user via function)
        // 25,26 : cx, cy // locally defined position of true center of sprite relative to origin
        //    (dependant on scale/shear/center/dimension)
        // 27,28 : u1, v1 // locally defined position of top-left vertex relative to center of sprite.
        //    (dependant on scale/shear/dimension)
        // 29,30 : u2, v2 // locally defined position of top-right vertex relative to center of sprite.
        //    (dependant on scale/shear/dimension)
        // 31,32 : px, py // relative defined position of true center of sprite relative to origin
        //    (dependant on rotation and cx,cy)
        // 33,34 : x1, y1 // relative defined position of top-left vertex relative to center of sprite.
        //    (dependant on rotation and u1,v1)
        // 35,36 : x2, y2 // relative defined position of top-right vertex relative to center of sprite.
        //    (dependant on rotation and u2,v2)
        // 37 : Squared epsilon to consider rotations equal based on dimensions.

        var s = new Draw2DSprite();
        var data = s.data = new Draw2D.floatArray(38);

        // texture (not optional)
        var texture = s._texture = params.texture || null;

        // position (optional, default 0,0)
        s.x = (params.x || 0.0);
        s.y = (params.y || 0.0);

        // rotation (optional, default 0)
        s.rotation = data[16] = (params.rotation || 0.0);

        // colour (optional, default [1,1,1,1])
        var color = params.color;
        data[8]  = (color ? color[0] : 1.0);
        data[9]  = (color ? color[1] : 1.0);
        data[10] = (color ? color[2] : 1.0);
        data[11] = (color ? color[3] : 1.0);

        // uvRect (optional, default texture rectangle)
        var uvRect = params.textureRectangle;
        var iwidth  = (texture ? 1 / texture.width  : 1);
        var iheight = (texture ? 1 / texture.height : 1);
        data[12] = (uvRect ? (uvRect[0] * iwidth)  : 0.0);
        data[13] = (uvRect ? (uvRect[1] * iheight) : 0.0);
        data[14] = (uvRect ? (uvRect[2] * iwidth)  : 1.0);
        data[15] = (uvRect ? (uvRect[3] * iheight) : 1.0);

        // dimensions / 2 (default texture dimensions)
        data[17] = ((params.width  !== undefined) ? params.width  : texture.width)  * 0.5;
        data[18] = ((params.height !== undefined) ? params.height : texture.height) * 0.5;

        // scale (default [1,1])
        var scale = params.scale;
        data[19] = (scale ? scale[0] : 1.0);
        data[20] = (scale ? scale[1] : 1.0);

        // shear (default [0,0])
        var shear = params.shear;
        data[21] = (shear ? shear[0] : 0.0);
        data[22] = (shear ? shear[1] : 0.0);

        // origin (default dimensions / 2)
        var origin = params.origin;
        data[23] = (origin ? origin[0] : data[17]);
        data[24] = (origin ? origin[1] : data[18]);

        s._invalidate();
        return s;
    }
}

//
// Used in rectangle draw routines to compute data to be pushed into
// vertex buffers.
//
//function Draw2DSpriteData() {}
var Draw2DSpriteData = {
    setFromRotatedRectangle:
    function setFromRotatedRectangleFn(sprite, texture, rect, uvrect, color,
                                       rotation, origin)
    {
        debug.assert(rect.length === 4);
        var x1 = rect[0];
        var y1 = rect[1];
        var x2 = rect[2];
        var y2 = rect[3];

        if (!rotation)
        {
            sprite[0] = x1;
            sprite[1] = y1;
            sprite[2] = x2;
            sprite[3] = y1;
            sprite[4] = x1;
            sprite[5] = y2;
            sprite[6] = x2;
            sprite[7] = y2;
        }
        else
        {
            var cx, cy;
            if (origin)
            {
                debug.assert(origin.length === 2);
                cx = x1 + origin[0];
                cy = y1 + origin[1];
            }
            else
            {
                cx = 0.5 * (x1 + x2);
                cy = 0.5 * (y1 + y2);
            }

            var dx = x1 - cx;
            var dy = y1 - cy;

            var cos = Math.cos(rotation);
            var sin = Math.sin(rotation);
            var w = (x2 - x1);
            var h = (y2 - y1);

            sprite[0] = x1 = cx + (cos * dx - sin * dy);
            sprite[1] = y1 = cy + (sin * dx + cos * dy);
            sprite[2] = x1 + (cos * w);
            sprite[3] = y1 + (sin * w);
            sprite[4] = x1 - (sin * h);
            sprite[5] = y1 + (cos * h);
            sprite[6] = x1 + (cos * w - sin * h);
            sprite[7] = y1 + (sin * w + cos * h);
        }

        if (color)
        {
            debug.assert(color.length === 4);
            sprite[8]  = color[0];
            sprite[9]  = color[1];
            sprite[10] = color[2];
            sprite[11] = color[3];
        }
        else
        {
            sprite[8] = sprite[9] = sprite[10] = sprite[11] = 1.0;
        }

        if (uvrect && texture)
        {
            debug.assert(uvrect.length === 4);
            var iwidth  = 1 / texture.width;
            var iheight = 1 / texture.height;
            sprite[12] = uvrect[0] * iwidth;
            sprite[13] = uvrect[1] * iheight;
            sprite[14] = uvrect[2] * iwidth;
            sprite[15] = uvrect[3] * iheight;
        }
        else
        {
            sprite[12] = sprite[13] = 0;
            sprite[14] = sprite[15] = 1;
        }
    },

    create: function draw2DSpriteFn()
    {
        // x1 y1 x2 y2 x3 y3 x4 y4 - vertices [0,8)
        // cr cg cb ca u1 v1 u2 v2 - normalized color + texture [8,16)
        return new Draw2D.floatArray(16);
    }
};

//
// Draw2D
//

interface Draw2DRenderTargetParams
{
    name?       : string;
    backBuffer? : boolean;
    width?      : number;
    height?     : number;
}

interface Draw2DRenderTarget
{
    managed      : boolean;
    renderTarget : RenderTarget;
    texture      : Texture;
    backBuffer   : boolean;
    actualWidth  : number;
    actualHeight : number;
}

// params : {
//    graphicsDevice : gd,
//    blendModes : { // optional
//       name : Technique,
//       **repeated**
//    }
// }
interface Draw2DParameters
{
    graphicsDevice    : GraphicsDevice;
    blendModes?       : { [name: string]: Technique; };
    initialGpuMemory? : number;
    maxGpuMemory?     : number;
}

class Draw2D
{
    static version = 7;
    forceUpdate = false;
    clearBackBuffer = false;

    graphicsDevice                : GraphicsDevice;

    width                         : number;
    height                        : number;
    scissorX                      : number;
    scissorY                      : number;
    scissorWidth                  : number;
    scissorHeight                 : number;
    clipOffsetX                   : number;
    clipOffsetY                   : number;
    clipScaleX                    : number;
    clipScaleY                    : number;
    viewScaleX                    : number;
    viewScaleY                    : number;
    viewportRectangle             : number[];
    spriteAngleFactor             : number;

    state                         : number;

    // Current sort and blend mode.
    sortMode                      : string;
    scaleMode                     : string;
    blendMode                     : string;
    nomipmaps                     : boolean;  // Only used in debug

    // Disjoint stack of modes for nested begins.
    sortModeStack                 : string[];
    blendModeStack                : string[];
    nomipmapsStack                : boolean[];  // Only used in debug

    // Set of render groups to be dispatched.
    drawGroups                    : Draw2DGroup[];
    numGroups                     : number;

    // Set of render groups for texture sort mode.  dictionary on
    // texture name.
    texLists                      : { [name: string]: Draw2DGroup; };

    // Cached reference to last retrieved group to accelerate texture
    // sort mode draw calls.
    texGroup                      : Draw2DGroup;

    // TODO                       : Conflicting uses of drawSprite
    drawSprite                    : any;

    // Sprite data instance used for rectangle draw calls.
    // drawSprite                 : Draw2DSpriteData;

    // Solid fill texture for draw calls that do not specify a
    // texture.
    defaultTexture                : Texture;

    // Draw call methods.  These are set based on current sort mode.
    draw                          : { (params: any): void; };  // TODO: params
    drawRaw                       : { (texture: Texture,
                                       multiSprite: number[],
                                       count?: number,
                                       offset?: number): void; };

    renderTargetStructs           : Draw2DRenderTarget[];
    renderTargetCount             : number;
    renderTargetIndex             : number;
    renderTargetTextureParameters : TextureParameters;

    currentRenderTarget           : Draw2DRenderTarget;

    currentTextureGroup           : Draw2DGroup;

    techniqueParameters           : TechniqueParameters;
    vertexBufferParameters        : VertexBufferParameters;
    vertexBuffer                  : VertexBuffer;
    indexBufferParameters         : IndexBufferParameters;
    indexBuffer                   : IndexBuffer;
    semantics                     : Semantics;
    renderTargetParams            : RenderTargetParameters;

    private blendModeTechniques   : {
        additive        : Technique;
        additivenomip   : Technique;
        alpha           : Technique;
        alphanomip      : Technique;
        opaque          : Technique;
        opaquenomip     : Technique;
    };

    copyTechnique                 : Technique;
    copyTechniqueParameters       : TechniqueParameters;
    copyVertexBufferParams        : VertexBufferParameters;
    copyVertexBuffer              : VertexBuffer;

    quadSemantics                 : Semantics;
    quadPrimitive                 : number;
    vertexBufferData              : any; // new Draw2D.floatArray()

    performanceData               : {
        gpuMemoryUsage : number;
        minBatchSize   : number;
        maxBatchSize   : number;
        avgBatchSize   : number;
        batchCount     : number;
        dataTransfers  : number;
    };

    maxGpuMemory                  : number;

    maxVertices                   : number;

    // number of bytes used per-sprite on cpu vertex buffers.
    cpuStride                     : number;

    // vertex buffer is in terms of number of vertices.  so we have a
    // stride of 4 rather than 128.
    gpuStride                     : number;

    // Array constructors
    // {
    //   new (size : number): any;
    //   new (data : number[]): any;
    // };
    static floatArray             : any;
    static uint16Array            : any;

    static defaultClearColor      : any = [0, 0, 0, 1]; // v4 or Array

    // Note that this code gets inserted into the constructor.
    defaultClearColor: any = Draw2D.defaultClearColor;

    // supported sort modes.
    sort = {
        deferred  : 'deferred',
        immediate : 'immediate',
        texture   : 'texture'
    };

    // supported scale modes.
    scale = {
        scale : 'scale',
        none  : 'none'
    };

    // Supported blend modes.
    blend = {
        additive   : 'additive',
        alpha      : 'alpha',
        opaque     : 'opaque'
    };

    // Map from mode name to the "nomip" equivalent.
    private nomipModes = {
        additive   : 'additivenomip',
        alpha      : 'alphanomip',
        opaque     : 'opaquenomip'
    };

    drawStates = {
        uninit: 0,
        ready : 1,
        draw  : 2
    };

    // Methods

    clear(clearColor?): boolean
    {
        if (this.state !== this.drawStates.ready)
        {
            return false;
        }

        var gd = this.graphicsDevice;
        if (this.currentRenderTarget)
        {
            if (!gd.beginRenderTarget(this.currentRenderTarget.renderTarget))
            {
                return false;
            }

            gd.clear(clearColor || this.defaultClearColor);
            gd.endRenderTarget();
        }
        else
        {
            gd.clear(clearColor || this.defaultClearColor);
        }

        return true;
    }

    clearBatch()
    {
        var texLists = this.texLists;
        for (var name in texLists)
        {
            if (texLists.hasOwnProperty(name))
            {
                texLists[name] = undefined;
            }
        }
        this.currentTextureGroup = undefined;
        this.numGroups = 0;
    }

    bufferSprite(buffer, sprite, index)
    {
        sprite._update(0);
        /* tslint:disable:no-bitwise */
        index <<= 4;
        /* tslint:enable:no-bitwise */

        var data = sprite.data;
        buffer[index]      = data[0];
        buffer[index + 1]  = data[1];
        buffer[index + 2]  = data[2];
        buffer[index + 3]  = data[3];
        buffer[index + 4]  = data[4];
        buffer[index + 5]  = data[5];
        buffer[index + 6]  = data[6];
        buffer[index + 7]  = data[7];
        buffer[index + 8]  = data[8];
        buffer[index + 9]  = data[9];
        buffer[index + 10] = data[10];
        buffer[index + 11] = data[11];
        buffer[index + 12] = data[12];
        buffer[index + 13] = data[13];
        buffer[index + 14] = data[14];
        buffer[index + 15] = data[15];
    }

    update()
    {
        var graphicsDevice = this.graphicsDevice;
        var width = this.width;
        var height = this.height;

        var graphicsDeviceWidth = graphicsDevice.width;
        var graphicsDeviceHeight = graphicsDevice.height;

        if (width !== graphicsDeviceWidth || height !== graphicsDeviceHeight || this.forceUpdate)
        {
            var viewWidth, viewHeight, viewX, viewY;
            var viewportRectangle = this.viewportRectangle;

            if (viewportRectangle)
            {
                viewX = viewportRectangle[0];
                viewY = viewportRectangle[1];
                viewWidth  = viewportRectangle[2] - viewX;
                viewHeight = viewportRectangle[3] - viewY;
            }
            else
            {
                viewX = 0;
                viewY = 0;
                viewWidth = graphicsDeviceWidth;
                viewHeight = graphicsDeviceHeight;
            }

            if ((viewWidth === graphicsDeviceWidth) && (viewHeight === graphicsDeviceHeight))
            {
                this.clearBackBuffer = false;
            }
            else
            {
                this.clearBackBuffer = true;
            }

            var target = this.currentRenderTarget;

            if (this.scaleMode === 'scale')
            {
                var viewAspectRatio = viewWidth / viewHeight;
                var graphicsDeviceAspectRatio = graphicsDeviceWidth / graphicsDeviceHeight;
                var calcViewWidth, calcViewHeight, diffWidth, diffHeight, halfDiffWidth, halfDiffHeight;

                if (graphicsDeviceAspectRatio > viewAspectRatio)
                {
                    calcViewWidth = Math.ceil((graphicsDeviceHeight / viewHeight) * viewWidth);
                    diffWidth = graphicsDeviceWidth - calcViewWidth;
                    halfDiffWidth = Math.floor(diffWidth * 0.5);

                    this.scissorX = halfDiffWidth;
                    this.scissorY = 0;
                    this.scissorWidth = calcViewWidth;
                    this.scissorHeight = graphicsDeviceHeight;

                    this.viewScaleX = viewWidth / calcViewWidth;
                    this.viewScaleY = viewHeight / graphicsDeviceHeight;

                    if (!target)
                    {
                        this.clipOffsetX = (halfDiffWidth / graphicsDeviceWidth * 2.0) - 1.0;
                        this.clipOffsetY = 1;
                        this.clipScaleX = (calcViewWidth / graphicsDeviceWidth * 2.0) / viewWidth;
                        this.clipScaleY = -2.0 / viewHeight;
                    }
                }
                else
                {
                    calcViewHeight = Math.ceil((graphicsDeviceWidth / viewWidth) * viewHeight);
                    diffHeight = graphicsDeviceHeight - calcViewHeight;
                    halfDiffHeight = Math.floor(diffHeight * 0.5);

                    this.scissorX = 0;
                    this.scissorY = halfDiffHeight;
                    this.scissorWidth = graphicsDeviceWidth;
                    this.scissorHeight = calcViewHeight;

                    this.viewScaleX = viewWidth / graphicsDeviceWidth;
                    this.viewScaleY = viewHeight / calcViewHeight;

                    if (!target)
                    {
                        this.clipOffsetX = -1.0;
                        this.clipOffsetY = 1 - ((halfDiffHeight / graphicsDeviceHeight) * 2.0);
                        this.clipScaleX = 2.0 / viewWidth;
                        this.clipScaleY = ((calcViewHeight / graphicsDeviceHeight) * -2.0) / viewHeight;
                    }
                }
            }
            else
            {
                this.viewScaleX = 1;
                this.viewScaleY = 1;

                if (!target)
                {
                    this.clipOffsetX = -1.0;
                    this.clipOffsetY = 1.0;
                    this.clipScaleX = 2.0 / graphicsDeviceWidth;
                    this.clipScaleY = -2.0 / graphicsDeviceHeight;
                }

                this.scissorX = 0;
                this.scissorY = (graphicsDeviceHeight - viewHeight);
                this.scissorWidth = viewWidth;
                this.scissorHeight = viewHeight;
            }

            this.spriteAngleFactor = Math.min(this.viewScaleX, this.viewScaleY);
            this.spriteAngleFactor *= this.spriteAngleFactor;

            this.width = graphicsDeviceWidth;
            this.height = graphicsDeviceHeight;

            var i = 0;
            var renderTargets = this.renderTargetStructs;
            var limit = renderTargets.length;
            for (i = 0; i < limit; i += 1)
            {
                this.validateTarget(renderTargets[i], this.scissorWidth, this.scissorHeight);
            }

            if (target)
            {
                this.clipOffsetX = -1.0;
                this.clipOffsetY = -1.0;
                this.clipScaleX = 2.0 * target.actualWidth / target.texture.width / viewWidth;
                this.clipScaleY = 2.0 * target.actualHeight / target.texture.height / viewHeight;
            }

            // Deal with viewports that are not started at (0,0)
            this.clipOffsetX -= viewX * this.clipScaleX;
            this.clipOffsetY -= viewY * this.clipScaleY;

            var clipSpace = this.techniqueParameters['clipSpace'];
            clipSpace[0] = this.clipScaleX;
            clipSpace[1] = this.clipScaleY;
            clipSpace[2] = this.clipOffsetX;
            clipSpace[3] = this.clipOffsetY;

            this.updateRenderTargetVbo(this.scissorX, this.scissorY, this.scissorWidth, this.scissorHeight);
            this.forceUpdate = false;
        }
    }

    getViewport(dst)
    {
        if (!dst)
        {
            dst = new Draw2D.floatArray(4);
        }
        var viewport = this.viewportRectangle;
        if (viewport)
        {
            dst[0] = viewport[0];
            dst[1] = viewport[1];
            dst[2] = viewport[2];
            dst[3] = viewport[3];
        }
        else
        {
            dst[0] = dst[1] = 0;
            dst[2] = this.graphicsDevice.width;
            dst[3] = this.graphicsDevice.height;
        }
        return dst;
    }

    getScreenSpaceViewport(dst?: any): any
    {
        if (!dst)
        {
            dst = new Draw2D.floatArray(4);
        }
        // ensure mapping is correct.
        this.update();

        dst[0] = this.scissorX;
        dst[1] = this.height - (this.scissorY + this.scissorHeight);
        dst[2] = dst[0] + this.scissorWidth;
        dst[3] = dst[1] + this.scissorHeight;
        return dst;
    }

    viewportMap(screenX, screenY, dst?): any
    {
        if (!dst)
        {
            dst = new Draw2D.floatArray(2);
        }
        // ensure mapping is correct.
        this.update();

        // webgl coordinates have flipped y.
        var scissorY = (this.height - this.scissorHeight - this.scissorY);

        dst[0] = (screenX - this.scissorX) * this.viewScaleX;
        dst[1] = (screenY - scissorY) * this.viewScaleY;

        var viewport = this.viewportRectangle;
        if (viewport)
        {
            dst[0] += viewport[0];
            dst[1] += viewport[1];
        }

        return dst;
    }

    viewportUnmap(x, y, dst?): any
    {
        if (!dst)
        {
            dst = new Draw2D.floatArray(2);
        }
        // ensure mapping is correct.
        this.update();

        var viewport = this.viewportRectangle;
        if (viewport)
        {
            x -= viewport[0];
            y -= viewport[1];
        }

        // webgl coordinates have flipped y.
        var scissorY = (this.height - this.scissorHeight - this.scissorY);

        dst[0] = (x / this.viewScaleX) + this.scissorX;
        dst[1] = (y / this.viewScaleY) + scissorY;
        return dst;
    }

    viewportClamp(point)
    {
        if (point)
        {
            var x = point[0];
            var y = point[1];

            var minX, minY, maxX, maxY;
            var viewport = this.viewportRectangle;
            if (viewport)
            {
                minX = viewport[0];
                minY = viewport[1];
                maxX = viewport[2];
                maxY = viewport[3];
            }
            else
            {
                minX = 0;
                minY = 0;
                maxX = this.graphicsDevice.width;
                maxY = this.graphicsDevice.height;
            }

            if (x < minX)
            {
                x = minX;
            }
            else if (x > maxX)
            {
                x = maxX;
            }

            if (y < minY)
            {
                y = minY;
            }
            else if (y > maxY)
            {
                y = maxY;
            }

            point[0] = x;
            point[1] = y;
        }

        return point;
    }

    configure(params)
    {
        if (this.state !== this.drawStates.ready)
        {
            return false;
        }

        var viewportRectangle = ("viewportRectangle" in params) ? params.viewportRectangle : this.viewportRectangle;

        var scaleMode = params.scaleMode;
        if (scaleMode !== undefined)
        {
            // check scaleMode is supported.
            if (!(scaleMode in this.scale))
            {
                return false;
            }
            if (scaleMode === 'scale' && !viewportRectangle)
            {
                return false;
            }
            this.scaleMode = scaleMode;
        }

        this.viewportRectangle = viewportRectangle;

        this.forceUpdate = true;
        this.update();

        return true;
    }

    destroy()
    {
        this.texLists = null;
        this.state = this.drawStates.uninit;

        this.graphicsDevice = null;

        if (this.vertexBuffer)
        {
            this.vertexBuffer.destroy();
        }
        if (this.indexBuffer)
        {
            this.indexBuffer.destroy();
        }

        this.copyVertexBuffer.destroy();

        var renderTargets = this.renderTargetStructs;
        while (renderTargets.length > 0)
        {
            var target = renderTargets.pop();
            target.texture.destroy();
            target.renderTarget.destroy();
            target.texture = null;
            target.renderTarget = null;
        }
    }

    begin(blendMode?: string, sortMode?: string, nomipmaps?: boolean)
    {
        // Check sort mode is well defined (or undefined signifying default)
        debug.assert((!sortMode) || (sortMode in this.sort),
                     "Bad sort mode");

        // Check blend mode is well defined (or undefined signifying default)
        debug.assert((!blendMode) || (blendMode in this.blend),
                     "Bad blend mode");

        //if there are render states left in the stack
        //and begin has been called without an end
        //draw previous data with current render state
        var firstTime = !this.sortMode;
        if (this.dispatch())
        {
            this.clearBatch();
        }

        if (firstTime)
        {
            if (this.state !== this.drawStates.ready)
            {
                return false;
            }

            // Check the buffers are correct before we render
            this.update();

            if (!this.currentRenderTarget)
            {
                this.graphicsDevice.setScissor(this.scissorX, this.scissorY, this.scissorWidth, this.scissorHeight);
            }
        }

        this.state = this.drawStates.draw;

        sortMode  = (sortMode)  ? sortMode  : (firstTime ? 'deferred' : this.sortMode);
        blendMode = (blendMode) ? blendMode : (firstTime ? 'opaque'   : this.blendMode);

        // For the default modes, automatically select the nomip
        // versions.  Custom modes will remain unchanged, so their
        // techniques must have state that is consistent with
        // 'nomipmaps'.  In debug mode, keep track of nomipmaps so
        // that we can assert as sprites are rendered.

        if (nomipmaps)
        {
            var nomipModes = this.nomipModes;
            if (nomipModes.hasOwnProperty(blendMode))
            {
                blendMode = nomipModes[blendMode];
            }
        }

        if (!firstTime)
        {
            this.sortModeStack.push(this.sortMode);
            this.blendModeStack.push(this.blendMode);
            if (debug)
            {
                this.nomipmapsStack.push(this.nomipmaps);
            }
        }
        this.sortMode = sortMode;
        this.blendMode = blendMode;
        if (debug)
        {
            this.nomipmaps = !!nomipmaps;
        }

        this.prepareSortMode(sortMode);
        this.graphicsDevice.setTechnique(this.blendModeTechniques[blendMode]);

        return true;
    }

    ////////////////////////////////////////////////////////////////////////////

    // append sprite data to group buffer.
    private _bufferSprite(group, sprite)
    {
        var vertexData = group.vertexBufferData;
        var vertexBuffer = this.vertexBuffer;

        var index = group.numVertices * vertexBuffer.stride;
        var total = index + (4 * vertexBuffer.stride);
        if (total >= vertexData.length)
        {
            // allocate new vertex buffer data array.
            var size = this.bufferSizeAlgorithm(total, this.cpuStride);
            var newData = new Draw2D.floatArray(size);

            // copy data from existing buffer.
            var i;
            for (i = 0; i < index; i += 1)
            {
                newData[i] = vertexData[i];
            }

            group.vertexBufferData = vertexData = newData;
        }

        var c1 = sprite[8];
        var c2 = sprite[9];
        var c3 = sprite[10];
        var c4 = sprite[11];
        var u1 = sprite[12];
        var v1 = sprite[13];
        var u2 = sprite[14];
        var v2 = sprite[15];

        vertexData[index]      = sprite[0];
        vertexData[index + 1]  = sprite[1];
        vertexData[index + 2]  = c1;
        vertexData[index + 3]  = c2;
        vertexData[index + 4]  = c3;
        vertexData[index + 5]  = c4;
        vertexData[index + 6]  = u1;
        vertexData[index + 7]  = v1;

        vertexData[index + 8]  = sprite[2];
        vertexData[index + 9]  = sprite[3];
        vertexData[index + 10] = c1;
        vertexData[index + 11] = c2;
        vertexData[index + 12] = c3;
        vertexData[index + 13] = c4;
        vertexData[index + 14] = u2;
        vertexData[index + 15] = v1;

        vertexData[index + 16] = sprite[4];
        vertexData[index + 17] = sprite[5];
        vertexData[index + 18] = c1;
        vertexData[index + 19] = c2;
        vertexData[index + 20] = c3;
        vertexData[index + 21] = c4;
        vertexData[index + 22] = u1;
        vertexData[index + 23] = v2;

        vertexData[index + 24] = sprite[6];
        vertexData[index + 25] = sprite[7];
        vertexData[index + 26] = c1;
        vertexData[index + 27] = c2;
        vertexData[index + 28] = c3;
        vertexData[index + 29] = c4;
        vertexData[index + 30] = u2;
        vertexData[index + 31] = v2;

        group.numVertices += 4;

        // increment number of indices in present subset.
        group.indices[group.numSets - 1] += 6;
    }

    private bufferMultiSprite(group, buffer, count?, offset?)
    {
        var vertexData = group.vertexBufferData;
        var vertexBuffer = this.vertexBuffer;

        var numSprites = (count === undefined) ? Math.floor(buffer.length / 16) : count;
        count = numSprites * 16;

        offset = (offset !== undefined ? offset : 0) * 16;

        var i;
        var index = (group.numVertices * vertexBuffer.stride);
        var total = index + (numSprites * 4 * vertexBuffer.stride);
        if (total >= vertexData.length)
        {
            // allocate new vertex buffer data array.
            var size = this.bufferSizeAlgorithm(total, this.cpuStride);
            var newData = new Draw2D.floatArray(size);

            // copy data from existing buffer.
            for (i = 0; i < index; i += 1)
            {
                newData[i] = vertexData[i];
            }

            group.vertexBufferData = vertexData = newData;
        }

        var limit = offset + count;
        for (i = offset; i < limit; i += 16)
        {
            var c1 = buffer[i + 8];
            var c2 = buffer[i + 9];
            var c3 = buffer[i + 10];
            var c4 = buffer[i + 11];
            var u1 = buffer[i + 12];
            var v1 = buffer[i + 13];
            var u2 = buffer[i + 14];
            var v2 = buffer[i + 15];

            vertexData[index]      = buffer[i];
            vertexData[index + 1]  = buffer[i + 1];
            vertexData[index + 2]  = c1;
            vertexData[index + 3]  = c2;
            vertexData[index + 4]  = c3;
            vertexData[index + 5]  = c4;
            vertexData[index + 6]  = u1;
            vertexData[index + 7]  = v1;

            vertexData[index + 8]  = buffer[i + 2];
            vertexData[index + 9]  = buffer[i + 3];
            vertexData[index + 10] = c1;
            vertexData[index + 11] = c2;
            vertexData[index + 12] = c3;
            vertexData[index + 13] = c4;
            vertexData[index + 14] = u2;
            vertexData[index + 15] = v1;

            vertexData[index + 16] = buffer[i + 4];
            vertexData[index + 17] = buffer[i + 5];
            vertexData[index + 18] = c1;
            vertexData[index + 19] = c2;
            vertexData[index + 20] = c3;
            vertexData[index + 21] = c4;
            vertexData[index + 22] = u1;
            vertexData[index + 23] = v2;

            vertexData[index + 24] = buffer[i + 6];
            vertexData[index + 25] = buffer[i + 7];
            vertexData[index + 26] = c1;
            vertexData[index + 27] = c2;
            vertexData[index + 28] = c3;
            vertexData[index + 29] = c4;
            vertexData[index + 30] = u2;
            vertexData[index + 31] = v2;

            index += 32;
        }

        group.numVertices += (numSprites * 4);
        // increment number of indices in present subset.
        group.indices[group.numSets - 1] += (numSprites * 6);
    }

    ////////////////////////////////////////////////////////////////////////////

    indexData(count)
    {
        var indexData = new Draw2D.uint16Array(count);
        var i;
        var vertexIndex = 0;
        for (i = 0; i < count; i += 6)
        {
            indexData[i]     = vertexIndex;
            indexData[i + 1] = vertexIndex + 1;
            indexData[i + 2] = vertexIndex + 2;
            indexData[i + 3] = vertexIndex + 1;
            indexData[i + 4] = vertexIndex + 2;
            indexData[i + 5] = vertexIndex + 3;
            vertexIndex += 4;
        }
        return indexData;
    }

    // upload group buffer to graphics device vertexBuffer.
    uploadBuffer(group, count, offset)
    {
        var vertexBuffer = this.vertexBuffer;
        var vertexBufferParameters = this.vertexBufferParameters;
        var graphicsDevice = this.graphicsDevice;
        var vertexData = group.vertexBufferData;

        var performanceData = this.performanceData;

        // Resize buffers.
        if (count > vertexBufferParameters.numVertices)
        {
            var newSize = this.bufferSizeAlgorithm(count, this.gpuStride);
            if (newSize > this.maxVertices)
            {
                newSize = this.maxVertices;
            }

            vertexBufferParameters.numVertices = newSize;
            this.vertexBuffer.destroy();
            this.vertexBuffer = vertexBuffer = graphicsDevice.createVertexBuffer(vertexBufferParameters);

            // 32 bytes per vertex.
            // 2 bytes per index, 1.5 indices per vertex.
            performanceData.gpuMemoryUsage = newSize * 35; // 32 + (1.5 * 2)

            newSize *= 1.5;

            // Set indices.
            var indexBufferParameters = this.indexBufferParameters;
            indexBufferParameters.data = this.indexData(newSize);
            indexBufferParameters.numIndices = newSize;
            this.indexBuffer.destroy();
            this.indexBuffer = graphicsDevice.createIndexBuffer(indexBufferParameters);
            indexBufferParameters.data = null;
            graphicsDevice.setIndexBuffer(this.indexBuffer);
        }

        performanceData.dataTransfers += 1;

        // Upload data.
        if (offset === 0)
        {
            vertexBuffer.setData(vertexData, 0, count);
        }
        else
        {
            var stride = vertexBuffer.stride;
            vertexBuffer.setData(vertexData.subarray(offset * stride, (offset + count) * stride), 0, count);
        }
    }

    ////////////////////////////////////////////////////////////////////////////

    drawRawImmediate(texture: Texture, multiSprite,
                     count?: number, offset?: number)
    {
        var group = this.drawGroups[0];
        group.textures[0] = texture || this.defaultTexture;
        group.indices[0] = 0;
        group.numSets = 1;
        this.numGroups = 1;

        this.bufferMultiSprite(group, multiSprite, count, offset);

        // Draw render group immediately.
        this.dispatch();
    }

    drawSpriteImmediate(sprite)
    {
        /* tslint:disable:no-bitwise */
        if (debug)
        {
            var _texture = sprite.getTexture();
            if (_texture)
            {
                debug.assert(this.nomipmaps ||
                             (0 === (_texture.width & (_texture.width - 1)) &&
                              0 === (_texture.height & (_texture.height - 1))),
                             "Cannot use mipmaps with NPOT textures");
            }
        }
        /* tslint:enable:no-bitwise */

        var group = this.drawGroups[0];
        group.textures[0] = sprite._texture || this.defaultTexture;
        group.indices[0] = 0;
        group.numSets = 1;
        this.numGroups = 1;

        sprite._update(this.spriteAngleFactor);
        this._bufferSprite(group, sprite.data);

        // Draw render group immediately.
        this.dispatch();
    }

    drawImmediate(params)
    {
        var texture = params.texture || this.defaultTexture;
        var destRect = params.destinationRectangle;
        var srcRect = params.sourceRectangle;
        var color = params.color;
        var rotation = params.rotation;

        var group = this.drawGroups[0];
        group.textures[0] = texture;
        group.indices[0] = 0;
        group.numSets = 1;
        this.numGroups = 1;

        var drawSprite = this.drawSprite;
        Draw2DSpriteData.setFromRotatedRectangle(
            drawSprite, texture, destRect, srcRect, color, rotation,
            params.origin);
        this._bufferSprite(group, drawSprite);

        // Draw render group immediately.
        this.dispatch();
    }

    ////////////////////////////////////////////////////////////////////////////

    drawRawDeferred(texture, multiSprite, count?, offset?)
    {
        texture = texture || this.defaultTexture;
        var group = this.drawGroups[0];
        this.numGroups = 1;
        // If present group draw list uses a different texture
        // We must start a new draw list.
        var numSets = group.numSets;
        if (numSets === 0 || group.textures[numSets - 1] !== texture)
        {
            group.textures[numSets] = texture;
            group.indices[numSets] = 0;
            group.numSets += 1;
        }

        this.bufferMultiSprite(group, multiSprite, count, offset);
    }

    drawSpriteDeferred(sprite)
    {
        /* tslint:disable:no-bitwise */
        if (debug)
        {
            var _texture = sprite.getTexture();
            if (_texture)
            {
                debug.assert(this.nomipmaps ||
                             (0 === (_texture.width & (_texture.width - 1)) &&
                              0 === (_texture.height & (_texture.height - 1))),
                             "Cannot use mipmaps with NPOT textures");
            }
        }
        /* tslint:enable:no-bitwise */

        var texture = sprite._texture || this.defaultTexture;

        var group = this.drawGroups[0];
        this.numGroups = 1;
        // If present group draw list uses a different texture
        // We must start a new draw list.
        var numSets = group.numSets;
        if (numSets === 0 || group.textures[numSets - 1] !== texture)
        {
            group.textures[numSets] = texture;
            group.indices[numSets] = 0;
            group.numSets += 1;
        }

        sprite._update(this.spriteAngleFactor);
        this._bufferSprite(group, sprite.data);
    }

    drawDeferred(params)
    {
        var texture = params.texture || this.defaultTexture;

        var group = this.drawGroups[0];
        this.numGroups = 1;
        // If present group draw list uses a different texture
        // We must start a new draw list.
        var numSets = group.numSets;
        if (numSets === 0 || group.textures[numSets - 1] !== texture)
        {
            group.textures[numSets] = texture;
            group.indices[numSets] = 0;
            group.numSets += 1;
        }

        var destRect = params.destinationRectangle;
        var srcRect = params.sourceRectangle;
        var color = params.color;
        var rotation = params.rotation;

        var drawSprite = this.drawSprite;
        Draw2DSpriteData.setFromRotatedRectangle(drawSprite, texture, destRect, srcRect, color, rotation, params.origin);

        this._bufferSprite(group, drawSprite);
    }

    ////////////////////////////////////////////////////////////////////////////

    drawRawTextured(texture, multiSprite, count?, offset?)
    {
        texture = texture || this.defaultTexture;
        var group;
        // If last call to drawTextured used the same texture, then we need not look up render group.
        if (this.currentTextureGroup !== undefined && this.currentTextureGroup.textures[0] === texture)
        {
            group = this.currentTextureGroup;
        }
        else
        {
            // Look up render group in texLists.
            var name = texture.name;
            var texLists = this.texLists;
            group = texLists[name];
            if (!group)
            {
                // Create new render group.
                group = this.drawGroups[this.numGroups];
                if (!group)
                {
                    group = Draw2DGroup.create();
                }
                this.drawGroups[this.numGroups] = texLists[name] = group;
                group.textures[0] = texture;
                group.indices[0] = 0;
                group.numSets = 1;
                this.numGroups += 1;
            }
            this.currentTextureGroup = group;
        }

        this.bufferMultiSprite(group, multiSprite, count, offset);
    }

    drawSpriteTextured(sprite)
    {
        /* tslint:disable:no-bitwise */
        if (debug)
        {
            var _texture = sprite.getTexture();
            if (_texture)
            {
                debug.assert(this.nomipmaps ||
                             (0 === (_texture.width & (_texture.width - 1)) &&
                              0 === (_texture.height & (_texture.height - 1))),
                             "Cannot use mipmaps with NPOT textures");
            }
        }
        /* tslint:enable:no-bitwise */

        var texture = sprite._texture || this.defaultTexture;

        var group;
        // If last call to drawTextured used the same texture, then we need not look up render group.
        if (this.currentTextureGroup !== undefined && this.currentTextureGroup.textures[0] === texture)
        {
            group = this.currentTextureGroup;
        }
        else
        {
            // Look up render group in texLists.
            var name = texture.name;
            var texLists = this.texLists;
            group = texLists[name];
            if (!group)
            {
                // Create new render group.
                group = this.drawGroups[this.numGroups];
                if (!group)
                {
                    group = Draw2DGroup.create();
                }
                this.drawGroups[this.numGroups] = texLists[name] = group;
                group.textures[0] = texture;
                group.indices[0] = 0;
                group.numSets = 1;
                this.numGroups += 1;
            }
            this.currentTextureGroup = group;
        }

        sprite._update(this.spriteAngleFactor);
        this._bufferSprite(group, sprite.data);
    }

    drawTextured(params)
    {
        var texture = params.texture || this.defaultTexture;

        var group;
        // If last call to drawTextured used the same texture, then we need not look up render group.
        if (this.currentTextureGroup !== undefined && this.currentTextureGroup.textures[0] === texture)
        {
            group = this.currentTextureGroup;
        }
        else
        {
            // Look up render group in texLists.
            var name = texture.name;
            var texLists = this.texLists;
            group = texLists[name];
            if (!group)
            {
                // Create new render group.
                group = this.drawGroups[this.numGroups];
                if (!group)
                {
                    group = Draw2DGroup.create();
                }
                this.drawGroups[this.numGroups] = texLists[name] = group;
                group.textures[0] = texture;
                group.indices[0] = 0;
                group.numSets = 1;
                this.numGroups += 1;
            }
            this.currentTextureGroup = group;
        }

        var destRect = params.destinationRectangle;
        var srcRect = params.sourceRectangle;
        var color = params.color;
        var rotation = params.rotation;

        var drawSprite = this.drawSprite;
        Draw2DSpriteData.setFromRotatedRectangle(drawSprite, texture, destRect, srcRect, color, rotation, params.origin);

        this._bufferSprite(group, drawSprite);
    }

    ////////////////////////////////////////////////////////////////////////////

    prepareSortMode(sortMode)
    {
        if (sortMode === 'deferred')
        {
            this.draw = this.drawDeferred;
            this.drawSprite = this.drawSpriteDeferred;
            this.drawRaw = this.drawRawDeferred;
        }
        else if (sortMode === 'immediate')
        {
            this.draw = this.drawImmediate;
            this.drawSprite = this.drawSpriteImmediate;
            this.drawRaw = this.drawRawImmediate;
        }
        else
        {
            this.draw = this.drawTextured;
            this.drawSprite = this.drawSpriteTextured;
            this.drawRaw = this.drawRawTextured;
        }
    }

    ////////////////////////////////////////////////////////////////////////////

    end()
    {
        if (this.state !== this.drawStates.draw)
        {
            return false;
        }

        //dispatch objects to the graphics card
        if (this.dispatch())
        {
            this.clearBatch();
        }

        if (this.blendModeStack.length !== 0)
        {
            this.blendMode = this.blendModeStack.pop();
            this.sortMode = this.sortModeStack.pop();
            this.prepareSortMode(this.sortMode);
            this.graphicsDevice.setTechnique(this.blendModeTechniques[this.blendMode]);
        }
        else
        {
            this.blendMode = undefined;
            this.sortMode = undefined;
            this.state = this.drawStates.ready;
        }

        return true;
    }

    dispatch()
    {
        // Nothing to dispatch.
        var numGroups = this.numGroups;
        if (numGroups === 0)
        {
            return false;
        }

        var graphicsDevice = this.graphicsDevice;
        var techniqueParameters = this.techniqueParameters;
        graphicsDevice.setIndexBuffer(this.indexBuffer);

        var drawGroups = this.drawGroups;
        var renderTargetUsed = false;
        if (this.currentRenderTarget)
        {
            renderTargetUsed = graphicsDevice.beginRenderTarget(this.currentRenderTarget.renderTarget);
        }

        var performanceData = this.performanceData;

        var texture : Texture;
        var i;
        for (i = 0; i < numGroups; i += 1)
        {
            var group = drawGroups[i];

            var textures = group.textures;
            var indices = group.indices;
            var setIndex = 0;

            var vindex = 0;
            var vlimit = group.numVertices;
            while (vindex < vlimit)
            {
                // number of vertices remaining.
                var vcount = vlimit - vindex;
                if (vcount > this.maxVertices)
                {
                    vcount = this.maxVertices;
                }

                // Upload group vertex sub-buffer to graphics device.
                this.uploadBuffer(group, vcount, vindex);
                graphicsDevice.setStream(this.vertexBuffer, this.semantics);

                // sprite uses 4 vertices, and 6 indices
                // so for 'vcount' number of vertices, we have vcount * 1.5 indices
                var ilimit = vcount * 1.5;
                var iindex = 0;
                while (iindex < ilimit) {
                    texture = textures[setIndex];
                    techniqueParameters['texture'] = texture;
                    techniqueParameters['npottexture'] = texture;

                    // number of indices remaining to render.
                    var icount = ilimit - iindex;
                    if (icount >= indices[setIndex])
                    {
                        // finish rendering sub list.
                        icount = indices[setIndex];
                        setIndex += 1;
                    }
                    else
                    {
                        // sub list still has remaining indices to render.
                        indices[setIndex] -= icount;
                    }

                    var batchSize = icount / 6;
                    if (performanceData.batchCount === 0)
                    {
                        performanceData.minBatchSize = batchSize;
                        performanceData.maxBatchSize = batchSize;
                        performanceData.avgBatchSize = batchSize;
                        performanceData.batchCount = 1;
                    }
                    else
                    {
                        if (batchSize < performanceData.minBatchSize)
                        {
                            performanceData.minBatchSize = batchSize;
                        }
                        if (batchSize > performanceData.maxBatchSize)
                        {
                            performanceData.maxBatchSize = batchSize;
                        }
                        performanceData.avgBatchSize *= performanceData.batchCount;
                        performanceData.avgBatchSize += batchSize;
                        performanceData.batchCount += 1;
                        performanceData.avgBatchSize /= performanceData.batchCount;
                    }

                    graphicsDevice.setTechniqueParameters(techniqueParameters);

                    /* tslint:disable:no-bitwise */
                    if (icount === 6)
                    {
                        graphicsDevice.draw(graphicsDevice.PRIMITIVE_TRIANGLE_STRIP, 4, ((iindex / 6) << 2));
                    }
                    else
                    {
                        graphicsDevice.drawIndexed(graphicsDevice.PRIMITIVE_TRIANGLES, icount, iindex);
                    }
                    /* tslint:enable:no-bitwise */

                    iindex += icount;
                }

                vindex += vcount;
            }

            group.numSets = 0;
            group.numVertices = 0;
        }

        if (this.currentRenderTarget && renderTargetUsed)
        {
            graphicsDevice.endRenderTarget();
        }

        return true;
    }

    bufferSizeAlgorithm(target, stride)
    {
        // scale factor of 2 is asymtopically optimal in terms of number of resizes
        // performed and copies performed, but we want to try and conserve memory
        // and so choose a less optimal 1.25 so that buffer will never be too much
        // larger than necessary.
        var factor = 1.25;

        // We size buffer to the next power of the factor which is >= target
        var logf = Math.ceil(Math.log(target) / Math.log(factor));
        var size = Math.floor(Math.pow(factor, logf));

        // Additionally ensure that we always take a multiple of of the stride
        // to avoid wasted bytes that could never be used.
        return (stride * Math.ceil(size / stride));
    }

    updateRenderTargetVbo(viewX, viewY, viewWidth, viewHeight)
    {
        var graphicsDevice = this.graphicsDevice;
        var halfGraphicsDeviceWidth = 0.5 * graphicsDevice.width;
        var halfGraphicsDeviceHeight = 0.5 * graphicsDevice.height;

        //
        // Update the VBO for the presentRenderTarget
        //
        var vertexBuffer = this.copyVertexBuffer;

        var left = (viewX - halfGraphicsDeviceWidth) / halfGraphicsDeviceWidth;
        var right = (viewX + viewWidth - halfGraphicsDeviceWidth) / halfGraphicsDeviceWidth;
        var topv = (viewY - halfGraphicsDeviceHeight) / halfGraphicsDeviceHeight;
        var bottom = (viewY + viewHeight - halfGraphicsDeviceHeight) / halfGraphicsDeviceHeight;

        var vertexData = this.vertexBufferData;
        vertexData[0] = left;
        vertexData[1] = bottom;
        vertexData[2] = 0.0;
        vertexData[3] = 1.0;

        vertexData[4] = left;
        vertexData[5] = topv;
        vertexData[6] = 0.0;
        vertexData[7] = 0.0;

        vertexData[8] = right;
        vertexData[9] = bottom;
        vertexData[10] = 1.0;
        vertexData[11] = 1.0;

        vertexData[12] = right;
        vertexData[13] = topv;
        vertexData[14] = 1.0;
        vertexData[15] = 0.0;

        vertexBuffer.setData(vertexData, 0, 4);
    }

    // always overallocate.
    /* tslint:disable:no-bitwise */
    static makePow2(dim)
    {
        var index = Math.log(dim) / Math.log(2);
        return (1 << Math.ceil(index));
    }
    /* tslint:enable:no-bitwise */

    createRenderTarget(params: Draw2DRenderTargetParams)
    {
        var gd = this.graphicsDevice;
        var renderTargets = this.renderTargetStructs;
        var index = renderTargets.length;

        var name = (params && params.name) ? params.name : ("RenderTarget#" + index);
        var backBuffer = (params && params.backBuffer !== undefined) ? params.backBuffer : true;
        var matchScreen = (params.width === undefined || params.height === undefined);

        var texParams = this.renderTargetTextureParameters;
        texParams.name = name;

        var width  = (matchScreen) ? gd.width  : params.width;
        var height = (matchScreen) ? gd.height : params.height;

        var makePow2 = Draw2D.makePow2;
        texParams.width  = makePow2(width);
        texParams.height = makePow2(height);

        var texture = gd.createTexture(texParams);
        var targetParams = this.renderTargetParams;
        targetParams.colorTexture0 = texture;
        var renderTarget = gd.createRenderTarget(targetParams);

        renderTargets.push({
            managed : matchScreen,
            renderTarget : renderTarget,
            texture : texture,
            backBuffer : backBuffer,
            actualWidth  : (backBuffer ? width  : texture.width),
            actualHeight : (backBuffer ? height : texture.height)
        });

        return index;
    }

    validateTarget(target, viewWidth, viewHeight)
    {
        if (target.managed)
        {
            var tex = target.texture;
            if (target.backBuffer)
            {
                target.actualWidth = viewWidth;
                target.actualHeight = viewHeight;
            }
            var makePow2 = Draw2D.makePow2;
            viewWidth = makePow2(viewWidth);
            viewHeight = makePow2(viewHeight);

            if (!target.backBuffer)
            {
                target.actualWidth = viewWidth;
                target.actualHeight = viewHeight;
            }
            if (tex.width !== viewWidth || tex.height !== viewHeight)
            {
                var texParams = this.renderTargetTextureParameters;
                var targetParams = this.renderTargetParams;

                texParams.name = tex.name;
                texParams.width  = viewWidth;
                texParams.height = viewHeight;

                tex.destroy();
                target.renderTarget.destroy();

                var graphicsDevice = this.graphicsDevice;
                target.texture = graphicsDevice.createTexture(texParams);
                targetParams.colorTexture0 = target.texture;
                target.renderTarget = graphicsDevice.createRenderTarget(targetParams);
            }
        }
    }

    setBackBuffer()
    {
        if (this.state !== this.drawStates.ready)
        {
            return false;
        }

        this.currentRenderTarget = null;
        this.forceUpdate = true;

        return true;
    }

    getRenderTargetTexture(renderTargetIndex)
    {
        var renderTargets = this.renderTargetStructs;
        if (renderTargetIndex < 0 || renderTargetIndex >= renderTargets.length)
        {
            return null;
        }

        return renderTargets[renderTargetIndex].texture;
    }

    getRenderTarget(renderTargetIndex)
    {
        var renderTargets = this.renderTargetStructs;
        if (renderTargetIndex < 0 || renderTargetIndex >= renderTargets.length)
        {
            return null;
        }

        return renderTargets[renderTargetIndex].renderTarget;
    }

    setRenderTarget(renderTargetIndex)
    {
        var renderTargets = this.renderTargetStructs;
        if (renderTargetIndex < 0 || renderTargetIndex >= renderTargets.length)
        {
            return false;
        }

        if (this.state !== this.drawStates.ready)
        {
            return false;
        }

        this.currentRenderTarget = renderTargets[renderTargetIndex];
        this.forceUpdate = true;

        return true;
    }

    copyRenderTarget(renderTargetIndex)
    {
        if (this.state !== this.drawStates.ready)
        {
            return false;
        }

        var renderTargets = this.renderTargetStructs;
        if (renderTargetIndex < 0 || renderTargetIndex >= renderTargets.length)
        {
            return false;
        }

        // Check the buffers are correct before we render.
        this.update();

        if (!this.currentRenderTarget)
        {
            this.graphicsDevice.setScissor(this.scissorX, this.scissorY, this.scissorWidth, this.scissorHeight);
        }

        var graphicsDevice = this.graphicsDevice;
        var target = renderTargets[renderTargetIndex];
        var tex = target.texture;

        var technique = this.copyTechnique;
        var params = this.copyTechniqueParameters;
        var copyUVScale = params['copyUVScale'];
        copyUVScale[0] = target.actualWidth / tex.width;
        copyUVScale[1] = target.actualHeight / tex.height;
        params['copyFlip'] = (!this.currentRenderTarget ? -1.0 : 1.0);
        params['inputTexture0'] = tex;

        var renderTargetUsed = false;
        var currentTarget = this.currentRenderTarget;
        var vbo = this.copyVertexBuffer;
        if (currentTarget)
        {
            renderTargetUsed = graphicsDevice.beginRenderTarget(currentTarget.renderTarget);
        }

        graphicsDevice.setTechnique(technique);
        graphicsDevice.setTechniqueParameters(params);

        graphicsDevice.setStream(vbo, this.quadSemantics);
        graphicsDevice.draw(this.quadPrimitive, 4, 0);

        if (currentTarget && renderTargetUsed)
        {
            graphicsDevice.endRenderTarget();
        }

        return true;
    }

    resetPerformanceData()
    {
        var data = this.performanceData;
        data.minBatchSize = data.maxBatchSize = data.avgBatchSize = undefined;
        data.batchCount = 0;
        data.dataTransfers = 0;
    }

    // Constructor function
    static create(params: Draw2DParameters): Draw2D
    {
        var o = new Draw2D();
        var gd = o.graphicsDevice = params.graphicsDevice;

        // Current sort and blend mode.
        o.sortMode  = undefined;
        o.blendMode = undefined;
        // Disjoint stack of modes for nested begins.
        o.sortModeStack  = [];
        o.blendModeStack = [];

        if (debug)
        {
            o.nomipmaps = false;
            o.nomipmapsStack = [];
        }

        // Set of render groups to be dispatched.
        o.drawGroups = [Draw2DGroup.create()];
        o.numGroups = 0;

        // Set of render groups for texture sort mode.
        // dictionary on texture name.
        o.texLists = {};
        // Cached reference to last retrieved group to accelerate
        // texture sort mode draw calls.
        o.texGroup = undefined;

        // Sprite data instance used for rectangle draw calls.
        o.drawSprite = Draw2DSpriteData.create();

        // Solid fill texture for draw calls that do not specify a texture.
        o.defaultTexture = gd.createTexture({
            name : "DefaultDraw2DTexture",
            width : 1,
            height : 1,
            depth : 1,
            format : "L8",
            cubemap : false,
            mipmaps : true,
            renderable : false,
            dynamic : false,
            data : [0xff]
        });

        // Draw call methods.
        // These are set based on current sort mode.
        o.draw = undefined;
        o.drawSprite = undefined;
        o.drawRaw = undefined;

        // Generated from assets/shaders/draw2D.cgfx
        var shader = gd.createShader(draw2D_cgfx);

        // Mapping from blend mode name to Technique object.
        o.blendModeTechniques = {
            additive       : shader.getTechnique("additive"),
            additivenomip  : shader.getTechnique("additivenomip"),
            alpha          : shader.getTechnique("alpha"),
            alphanomip     : shader.getTechnique("alphanomip"),
            opaque         : shader.getTechnique("opaque"),
            opaquenomip    : shader.getTechnique("opaquenomip")
        };

        debug.assert(o.blendModeTechniques.additive);
        debug.assert(o.blendModeTechniques.additivenomip);
        debug.assert(o.blendModeTechniques.alpha);
        debug.assert(o.blendModeTechniques.alphanomip);
        debug.assert(o.blendModeTechniques.opaque);
        debug.assert(o.blendModeTechniques.opaquenomip);

        // Append techniques and supported blend modes with user
        // supplied techniques.

        if (params.blendModes)
        {
            for (var name in params.blendModes)
            {
                if (params.blendModes.hasOwnProperty(name))
                {
                    o.blend[name] = name;
                    o.blendModeTechniques[name] = params.blendModes[name];
                }
            }
        }

        // Blending techniques.
        o.techniqueParameters = gd.createTechniqueParameters({
            clipSpace: new Draw2D.floatArray(4),
            texture: null,
            npottexture: null
        });

        // Current render target
        o.currentRenderTarget = null;
        o.renderTargetStructs = [];

        o.state = o.drawStates.ready;

        o.scaleMode = 'none';
        o.blendMode = 'opaque';

        // View port, back buffer and managed render target values.
        o.width = 0;
        o.height = 0;

        o.scissorX = 0;
        o.scissorY = 0;
        o.scissorWidth = o.graphicsDevice.width;
        o.scissorHeight = o.graphicsDevice.height;

        o.clipOffsetX = -1.0;
        o.clipOffsetY = 1;
        o.clipScaleX = 2.0 / o.graphicsDevice.width;
        o.clipScaleY = -2.0 / o.graphicsDevice.height;

        o.viewScaleX = 1;
        o.viewScaleY = 1;

        // GPU Memory.
        // -----------

        var initial = (params.initialGpuMemory ? params.initialGpuMemory : 0);
        if (initial < 140)
        {
            // 140 = minimum that can be used to draw a single sprite.
            initial = 140;
        }
        if (initial > 2293760)
        {
            // 2293760 = maximum that can ever be used in 16bit indices.
            initial = 2293760;
        }

        o.performanceData = {
            gpuMemoryUsage : initial,
            minBatchSize : 0,
            maxBatchSize : 0,
            avgBatchSize : 0,
            batchCount : 0,
            dataTransfers : 0
        };

        o.maxGpuMemory = (params.maxGpuMemory ? params.maxGpuMemory : 2293760);
        if (o.maxGpuMemory < initial)
        {
            o.maxGpuMemory = initial;
        }

        var initialVertices = Math.floor(initial / 140) * 4;
        o.maxVertices = Math.floor(o.maxGpuMemory / 140) * 4;
        if (o.maxVertices > 65536)
        {
            o.maxVertices = 65536;
        }

        // number of bytes used per-sprite on cpu vertex buffers.
        o.cpuStride = 64;

        // vertex buffer is in terms of number of vertices.
        // so we have a stride of 4 rather than 128.
        o.gpuStride = 4;

        // Index and vertex buffer setup.
        o.vertexBufferParameters = {
            numVertices: initialVertices,
            attributes: [gd.VERTEXFORMAT_FLOAT2, gd.VERTEXFORMAT_FLOAT4, gd.VERTEXFORMAT_FLOAT2],
            'transient': true
        };
        o.vertexBuffer = gd.createVertexBuffer(o.vertexBufferParameters);

        o.semantics = gd.createSemantics([gd.SEMANTIC_POSITION, gd.SEMANTIC_COLOR, gd.SEMANTIC_TEXCOORD0]);
        o.indexBufferParameters = {
            numIndices: (initialVertices * 1.5),
            format: gd.INDEXFORMAT_USHORT,
            dynamic: false,
            data : o.indexData((initialVertices * 1.5))
        };
        o.indexBuffer = gd.createIndexBuffer(o.indexBufferParameters);
        o.indexBufferParameters.data = null;

        // Render Target API
        // -----------------

        // Objects and values used in render target management.
        o.renderTargetIndex = 0;
        o.renderTargetCount = 0;

        o.renderTargetTextureParameters = {
            name   : '',
            width  : 0,
            height : 0,
            depth  : 1,
            format     : "R8G8B8A8",
            cubemap    : false,
            mipmaps    : true,
            renderable : true,
            dynamic    : true
        };

        o.renderTargetParams = {
            colorTexture0 : null
        };

        // Render Target copying.
        // ----------------------

        // Copy technique for copyRenderTarget
        o.copyTechnique = shader.getTechnique("copy");
        o.copyTechniqueParameters = gd.createTechniqueParameters({
            inputTexture0 : null,
            copyFlip : 1,
            copyUVScale : new Draw2D.floatArray([1, 1])
        });

        // Objects used in copyRenderTarget method.
        o.quadSemantics = gd.createSemantics([gd.SEMANTIC_POSITION, gd.SEMANTIC_TEXCOORD0]);
        o.quadPrimitive = gd.PRIMITIVE_TRIANGLE_STRIP;

        o.copyVertexBufferParams = {
            numVertices: 4,
            attributes: [gd.VERTEXFORMAT_FLOAT2, gd.VERTEXFORMAT_FLOAT2],
            'transient': true
        };
        o.copyVertexBuffer = gd.createVertexBuffer(o.copyVertexBufferParams);

        // updateRenderTargetVBO
        // ---------------------

        o.vertexBufferData = new Draw2D.floatArray([-1.0, -1.0, 0.0, 0.0,
                                                    1.0, -1.0, 1.0, 0.0,
                                                    -1.0,  1.0, 0.0, 1.0,
                                                    1.0,  1.0, 1.0, 1.0]);

        return o;
    }
}

// Detect correct typed arrays
(function () {
    Draw2D.uint16Array = function(arg)
    {
        if (arguments.length === 0)
        {
            return [];
        }

        var i, ret;
        if (typeof arg === "number")
        {
            ret = new Array(arg);
        }
        else
        {
            ret = [];
            for (i = 0; i < arg.length; i += 1)
            {
                ret[i] = arg[i];
            }
        }
        return ret;
    };

    var testArray;
    var textDescriptor;

    if (typeof Uint16Array !== "undefined")
    {
        testArray = new Uint16Array(4);
        textDescriptor = Object.prototype.toString.call(testArray);
        if (textDescriptor === '[object Uint16Array]')
        {
            Draw2D.uint16Array = Uint16Array;
        }
    }

    Draw2D.floatArray = function (arg) {
        if (arguments.length === 0)
        {
            return [];
        }

        var i, ret;
        if (typeof arg === "number")
        {
            ret = new Array(arg);
        }
        else
        {
            ret = [];
            for (i = 0; i < arg.length; i += 1)
            {
                ret[i] = arg[i];
            }
        }
        return ret;
    };

    if (typeof Float32Array !== "undefined")
    {
        testArray = new Float32Array(4);
        textDescriptor = Object.prototype.toString.call(testArray);
        if (textDescriptor === '[object Float32Array]')
        {
            Draw2D.floatArray = Float32Array;
            Draw2D.defaultClearColor =
                new Float32Array(Draw2D.defaultClearColor);
        }
    }
}());
